package edu.mercer.sse.tlocal;

import java.util.Map;
import java.util.logging.Level;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheStats;
import com.google.common.collect.Maps;

import edu.mercer.sse.logging.CustomLogger;
import edu.mercer.sse.logging.LoggerCustom;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Thread Object Proxy.<p>
 * 
 * Purpose: single type of object global to a thread.<p>
 * 
 * Singleton: only one thread object proxy per object type exists for each 
 * 	thread.<p>
 *
 * @see ThreadObjectProxification
 */

public class ThreadObjectProxy<I,X> implements ThreadObjectProxification<I,X>
{
	/** Log: file written to disk in current path root folder.<p> */
	
	private CustomLogger _log = LoggerCustom.obtainLog("thread_obj_prxy", 
			Level.ALL);
	
	/**
	 * Cache of caches: control creation of new ThreadObjectProxy instances,
	 * 	limiting to one per object type, per thread.<p>
	 * 
	 * Singleton, shared by all threads. Stores different types of 
	 * 	ThreadObjectProxies, one for each type of object per thread.<p>
	 * 
	 * Threads are keys: when thread ends, any of its cache of stored objects' 
	 * 	state is eligible for eviction from this cache-of-cache's.<p>
	 * 
	 * Note: Guava Object Cache allows wildcards below and use of put method 
	 * 	with parameters defined by captures. WeakHashMap, ConcurrentHashMap... 
	 * 	require definition- Example: <Object,Object>  -in order to permit the 
	 * 	same put operation and avoid compiler errors.
	 * 
	 * @see #factory()
	 */
	
	private final static Cache<Thread,ThreadObjectProxification<?,?>> 
		_objectSetsCache = CacheBuilder.newBuilder()
									   .weakKeys()
									   .build();
	
	
	/**
	 * Simple Map as object cache as unique per thread.<p> 
	 * 
	 * Contains object states for this thread only for a single type.<p>
	 * 
	 * Generic I is key. Maps to global object instance. Instance is only 
	 * 	global to this thread.<p>
	 */
	
	private final Map<I,X> _cache = Maps.newHashMap();

	
	/**
	 * Private constructor.<p>
	 * 
	 * Construction controlled by Singleton cache. Only one proxy instance per 
	 * 	object type, per thread is supported.<p>
	 * 
	 * @see #factory() 
	 * @see #_objectSetsCache
	 */
	
	private ThreadObjectProxy()
	{
		super();
		
	}//constructor
	

	/**
	 * Factory.<p>
	 * 
	 * Restrict new instance creation to once per thread.<p>
	 * 
	 * @return  ThreadObjectProxification&#60;I,X&#62; instance
	 * 
	 * @see ThreadObjectProxification
	 */
	
	@SuppressWarnings("unchecked")
	public static <I,X>ThreadObjectProxification<I,X> factory()
	{
		if( _objectSetsCache.getIfPresent( Thread.currentThread() ) == null )
		{
			LoggerCustom.obtainLog("thread_obj_prxy", Level.ALL).fine(
					"creating set of thread object instances");
			
			_objectSetsCache.put( Thread.currentThread(), 
								  new ThreadObjectProxy<I,X>() );

		}
		else
		{
			LoggerCustom.obtainLog("thread_obj_prxy").fine("located "
					+ "existing set of thread object instances in Guava "
					+ "cache");
			
		}//if
		
		checkNotNull( _objectSetsCache.getIfPresent( Thread.currentThread() ), 
				"fail retrieve thread object proxy for this thread" );

		return ( ThreadObjectProxification<I,X> ) _objectSetsCache
				.getIfPresent( Thread.currentThread() );

	}//method
	

	@Override
	public X get( final I objectIndex )
	{		
		checkNotNull( objectIndex, "null object instance key, unable to "
				+ "attempt location in cache of stored object instances" );

		if( ! _cache.containsKey( objectIndex ) )
		{
			throw new RuntimeException( "object instance not found in Map " 
					+ "cache for this thread, this key: " + objectIndex );
			
		}//if
				
		return _cache.get( objectIndex );
		
	}//method

	
	@Override
	public ThreadObjectProxification<I,X> set( final I objectIndex, 
											   final X objectInstance )
	{
		/* 
		 * prevent overwrite of any existing thread-global instances while 
		 * 	allowing initialization
		 */
		
		if( ! _cache.containsKey( objectIndex ) ) 
		{	
			_cache.put( objectIndex, objectInstance );

			_log.fine( "adding new: " + objectIndex 
									  + " : with hashCode of " 
									  + objectInstance );

		}
		else
		{
			_log.fine( "located existing: " + objectIndex 
					  + " : with hashCode of " 
					  + objectInstance.hashCode() );
			
		}//if
		
		return this;
		
	}//method
	
	
	@SuppressWarnings("unchecked")
	@Override
	public ThreadObjectProxification<I,X> append( final I objectIndex, 
			   									  final X objectInstance )
	{
		if( ! _cache.containsKey( objectIndex ) ) 
			return set(objectIndex,objectInstance);
		
		if( objectInstance instanceof Integer )
		{
			Integer valueIn = (Integer)_cache.get(objectIndex);
			
			_cache.put(objectIndex, (X)(Object)( valueIn 
					+ (Integer)(objectInstance)));
			
		}
		else if( objectInstance instanceof Double )
		{
			Double valueIn = (Double)_cache.get(objectIndex);
		
			_cache.put(objectIndex, (X)(Object)( valueIn 
					+ (Double)(objectInstance)));
			
		}
		else if( objectInstance instanceof String )
		{
			String valueIn = (String)_cache.get(objectIndex);

			_cache.put(objectIndex, (X)(Object)( valueIn 
					+ (String)(objectInstance)));

		}
		else if( objectInstance instanceof StringBuilder )
		{
			StringBuilder valueIn = (StringBuilder)_cache.get(objectIndex);

			_cache.put(objectIndex, (X)(Object)( valueIn.append( 
					objectInstance.toString()))) ;
		
		}
		else
		{
			throw new RuntimeException("append operation: unsupported or "
					+ "unrecognized type: " 
					+ objectInstance);
			
		}//if	
		
		return this;
		
	}//method
	
	
	@Override
	public CacheStats getCacheStats()
	{
		return _objectSetsCache.stats();
		
	}//method


}//class
